package org.jpropelleralt.path.impl;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.node.Node;
import org.jpropelleralt.path.Path;
import org.jpropelleralt.path.PathIterator;
import org.jpropelleralt.path.PathStep;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.transformer.Transformer;

/**
 * A means of building a {@link Path} in a typesafe way, using
 * {@link Transformer}s for each stage of the path.
 * This uses a slightly restricted {@link Path}, where each 
 * iterated element is ALWAYS a {@link Ref}, as well as the
 * final element.
 *
 * @param <D>		The value type of the final {@link Ref}
 * 					reached by the {@link PathBuilder}
 */
public class PathBuilder<D extends Node> {

	//We use raw transformers so we can apply them to anything - we make
	//sure we only ever have valid types by the way we build instances
	//of this class
	@SuppressWarnings("rawtypes")
	private LinkedList<Transformer> transformers;
	private Ref<?> root;
	
	/**
	 * Create a new {@link PathBuilder}, starting 
	 * with no transform, just starting and ending
	 * at a first root {@link Ref}
	 * @param root		The root element, start of the path
	 */
	//See first suppression above
	@SuppressWarnings("rawtypes")
	private PathBuilder(Ref<D> root) {
		this(root, new LinkedList<Transformer>());
	}

	/**
	 * Create a {@link PathBuilder} with a specified root and
	 * list of {@link Transformer}s.
	 * The list may be empty, in which case root must be of type Ref<D>.
	 * If the list is NOT empty, please
	 * make sure that the list starts with a transformer that
	 * will accept the value in the root {@link Ref}, and ends with a {@link Transformer} 
	 * from something to Ref<D>, and also that all the {@link Transformer}s 
	 * (except the last) produce a Ref with the value type required by the next in the list.
	 * @param root				The root element, start of the path 
	 * @param transformers		List of transformers
	 */
	//See first suppression
	@SuppressWarnings("rawtypes")
	private PathBuilder(Ref<?> root, LinkedList<Transformer> transformers) {
		this.root = root;
		this.transformers = transformers;
	}

	/**
	 * Create a builder, which has no {@link Transformer}s yet
	 * @param root		The root element, start of the path
	 * @return			A new {@link PathBuilder}
	 * @param <D>		The type of value in the root
	 */
	public static <D extends Node> PathBuilder<D> create(Ref<D> root) {
		return new PathBuilder<D>(root);
	}
	

	/**
	 * Make a new {@link PathBuilder} that adds another {@link Transformer}
	 * to the end of the chain performed by this {@link PathBuilder}
	 * 
	 * @param <E>			The destination type of the {@link PathBuilder} we will produce
	 * @param nextTransform	The {@link Transformer} to add to this one
	 * @return				A {@link PathBuilder} that applies the same {@link Transformer}s, 
	 * 						in the same order, as this {@link PathBuilder}, then applies the 
	 * 						extra {@link Transformer} on the end
	 */
	//We make sure that we only accept a Transformer that will take any D as an input (since
	//it accepts <? super D>. We then make sure it produces something with a getter for
	//type E  - <? extends Prop<E>>. In this way we make sure that each Transformer
	//must provide data that the next will accept
	@SuppressWarnings("rawtypes")
	public <E extends Node> PathBuilder<E> via(Transformer<? super D, ? extends Ref<E>> nextTransform) {
		LinkedList<Transformer> newList = new LinkedList<Transformer>(transformers);
		newList.add(nextTransform);
		return new PathBuilder<E>(root, newList);
	}
	
	/**
	 * Make a new {@link PathBuilder} that adds several {@link Transformer}s
	 * to the end of the chain performed by this {@link PathBuilder}.
	 * The {@link Transformer}s each use a {@link Node} name to go from the
	 * {@link Node} to a named {@link Ref}. 

	 * 
	 * @param names			The names to follow from current {@link Ref}'s {@link Node} value 
	 * 						to get next {@link Ref}.
	 * @return				A {@link PathBuilder} that applies the same {@link Transformer}s, 
	 * 						in the same order, as this {@link PathBuilder}, then applies the 
	 * 						extra {@link Transformer}s on the end, based on supplied names.
	 */
	public PathBuilder<Node> via(final String... names) {
		if (names.length == 0) {
			throw new IllegalArgumentException("Must specify at least one name");
		}
		
		//This may look slightly odd - why two variables?
		//This is a (not too) messy way
		//of making sure we end up with a PathBuilder<Node>, but can
		//start with a PathBuilder<?>. After the first via() call we
		//know we will end up with a Node.
		PathBuilder<?> currentBuilder = this;
		PathBuilder<Node> lastBuilder = null;
		for (String nextName : names) {
			lastBuilder = currentBuilder.via(nextName);
			currentBuilder = lastBuilder; 
		}
		return lastBuilder;
	}
	
	/**
	 * Make a new {@link PathBuilder} that adds another {@link Transformer}
	 * to the end of the chain performed by this {@link PathBuilder}
	 * The {@link Transformer} uses a {@link Node} name to go from the
	 * {@link Node} to a named {@link Ref}. Since no value class is provided,
	 * this produces a {@link PathBuilder} leading to a {@link Node}, since
	 * we don't know what type of {@link Ref} the name will look up.  
	 * 
	 * @param nextName		The name to follow from current {@link Ref}'s {@link Node} value 
	 * 						to get next {@link Ref}.
	 * @return				A {@link PathBuilder} that applies the same {@link Transformer}s, 
	 * 						in the same order, as this {@link PathBuilder}, then applies an 
	 * 						extra {@link Transformer} on the end, based on supplied name.
	 */
	//See comments below
	@SuppressWarnings("unchecked")
	public PathBuilder<Node> via(final String nextName) {
		return via(new PathStep<D, Node>() {
			public Ref<Node> transform(D s) {
				Ref<?> ref = s.get(nextName);
				if (ref != null && ref.get() instanceof Node) {
					
					//At this point, we have checked that the Ref currently actually contains
					//a Node. We also know that when the Path and PathIterator instances we
					//produce access the Ref, all they will do is either:
					// * Call get() on it, and expect to get a Node - they will succeed, because
					//   we have just checked it contains a Node.
					// * Pass it out from the iterator as a Box - and we know it is definitely a Ref,
					//   so it is a Box.
					return ((Ref<Node>)ref);
				} else {
					return null;
				}
			};
		});
	}
	
	/**
	 * Make a new {@link PathBuilder} that adds a {@link Transformer}
	 * to the end of the chain performed by this {@link PathBuilder}.
	 * The {@link Transformer} uses a {@link Node} name to go from the
	 * {@link Node} to a named {@link Ref}. A valueClass is provided to
	 * allow for type safety - hence this only works with {@link Ref}s
	 * having a valueClass. If a {@link Ref} is looked up with a missing
	 * valueClass, or one that is not identical to the specified class,
	 * the {@link Transformer} returns null, making the whole system
	 * type-safe. 
	 * 
	 * @param <E>			The destination type of the {@link PathBuilder} we will produce
	 * @param valueClass	The class of value in the {@link Ref} to be looked up.
	 * @param nextName		The name to follow from current {@link Ref}'s {@link Node} value 
	 * 						to get next {@link Ref}.
	 * @return				A {@link PathBuilder} that applies the same {@link Transformer}s, 
	 * 						in the same order, as this {@link PathBuilder}, then applies an 
	 * 						extra {@link Transformer} on the end, based on supplied name.
	 */
	public <E extends Node> PathBuilder<E> via(final Class<E> valueClass, final String nextName) {
		if (valueClass == null) {
			throw new IllegalArgumentException("Cannot specify a null valueClass");
		}
		return via(new PathStep<D, E>() {
			//We can safely cast to Ref<E> since we check that the Ref has correct valueClass,
			//and a Ref cannot be created with a valueClass unless it's parametric type is exactly
			//the same as the valueClass
			@SuppressWarnings("unchecked")
			public Ref<E> transform(D s) {
				if (s == null) {
					return null;
				}
				Ref<?> ref = s.get(nextName);
				if (ref.valueClass() == valueClass) {
					return ((Ref<E>)ref);
				} else {
					return null;
				}
			};
		});
	}

	/**
	 * Produce a {@link Path} using the {@link Transformer}s used to create this
	 * builder, then the last {@link Transformer} specified
	 * 
	 * @param <Q> 			The type of value in the {@link Ref} at the end of the path
	 * @param lastTransform The last {@link Transformer} in the path
	 * @return				The {@link Path} itself
	 */
	public <Q> Path<Q> to(Transformer<? super D, ? extends Ref<Q>> lastTransform) {
		return new PathDefault<Q>(root, transformers, lastTransform);
	}

	/**
	 * Produce a {@link Path} using the {@link Transformer}s used to create this
	 * builder, then the last {@link Transformer} specified
	 * The {@link Transformer} uses a {@link Node} name to go from the
	 * {@link Node} to a named {@link Ref}. A valueClass is provided to
	 * allow for type safety - hence this only works with {@link Ref}s
	 * having a valueClass. If a {@link Ref} is looked up with a missing
	 * valueClass, or one that is not identical to the specified class,
	 * the {@link Transformer} returns null, making the whole system
	 * type-safe. 
	 * 
	 * @param <E> 			The type of value in the {@link Ref} at the end of the path
	 * @param valueClass	The class of value in the {@link Ref} to be looked up.
	 * @param nextName		The name to follow from current {@link Ref}'s {@link Node} value 
	 * 						to get final {@link Ref}.
	 * @return				The {@link Path} itself
	 */
	public <E> Path<E> to(final Class<E> valueClass, final String nextName) {
		if (valueClass == null) {
			throw new IllegalArgumentException("Cannot specify a null valueClass");
		}
		return to(new PathStep<D, E>() {
			//We can safely cast to Ref<E> since we check that the Ref has correct valueClass,
			//and a Ref cannot be created with a valueClass unless it's parametric type is exactly
			//the same as the valueClass
			@SuppressWarnings("unchecked")
			public Ref<E> transform(D s) {
				if (s == null) {
					return null;
				}
				Ref<?> ref = s.get(nextName);
				if (ref.valueClass() == valueClass) {
					return ((Ref<E>)ref);
				} else {
					return null;
				}
			};
		});
	}
	
	//#######################################################
	
	/**
	 * The type of {@link Path} produced by this builder
	 * 
	 * @param <D>		The type of value in the {@link Ref} at the end of the path
	 */
	private static class PathDefault<D> implements Path<D> {

		private final Transformer<?, ? extends Ref<D>> lastTransform;
		private final Ref<?> root;
		
		//We rely on valid Transformer types when created
		@SuppressWarnings("rawtypes")
		private final List<Transformer> transforms;
		
		//See first @SuppressWarnings("unchecked")
		@SuppressWarnings("rawtypes")
		private PathDefault(Ref<?> root, List<Transformer> transforms,
				Transformer<?, ? extends Ref<D>> lastTransform) {
			super();
			this.root = root;
			this.transforms = transforms;
			this.lastTransform = lastTransform;
		}

		@Override
		public PathIterator<D> iterator() {
			return new PathIteratorDefault<D>(root, transforms, lastTransform);
		}
		
	}
	
	//#######################################################
	
	/**
	 * {@link PathIterator} for {@link Path}s built using this builder
	 *
	 * @param <D>		The type of data in the last {@link Ref}
	 */
	private static class PathIteratorDefault<D> implements PathIterator<D> {

		private Ref<?> current;
		
		//We rely on anything constructing this class to have the correct type
		@SuppressWarnings("rawtypes")
		private final Transformer lastTransform;
		
		//See first suppression
		@SuppressWarnings("rawtypes")
		private Iterator<Transformer> iterator;
		private Ref<D> finalProp;
		
		//We start on the root itself
		private boolean onRoot = true;

		/**
		 * Create a new iterator
		 * @param root
		 * 		Root bean for the iteration
		 * @param transforms
		 * 		The transforms to follow first
		 * @param lastTransform
		 * 		The last transform to follow
		 */
		//We use raw transformers to avoid warnings - we know the transformers follow through correctly
		//since builder enforces this as it is built
		@SuppressWarnings("rawtypes")
		private PathIteratorDefault(Ref<?> root, List<Transformer> transforms,
				Transformer lastTransform) {
			super();
			this.lastTransform = lastTransform;
			iterator = transforms.iterator(); 
			current = root;
		}

		@Override
		public boolean hasNext() {
			//If we have hit null, no more elements
			if (current == null) {
				return false;
				
			//If we are still on the root (not yet returned root
			//from iterator) then we have a next element.
			} else if (onRoot) {
				return true;
				
			//Otherwise we have more elements if we have more transformers to apply.
			} else {
				return iterator.hasNext();
			}
		}

		//We know that we are constructed with valid Transformers to give the correct types at each step
		@SuppressWarnings("unchecked")
		@Override
		public Box next() {
			//If the current Ref is null, return null
			if (current == null) return null;
			
			//If we are on the root, then return it (And we are no longer on the root)
			if (onRoot) {
				onRoot = false;
				return current;
			}
			
			//We have returned the root, so each step from now on involves transforming
			//to the next element, via the next transformer
			
			//Note the current.get() - we always store which ref we are on,
			//but the transformers are all from the ref VALUE to the next ref.
			Object currentValue = current.get();

			//If we have a null value in a Ref, then we need to give up - just set
			//current to nul and return
			if (currentValue == null) {
				current = null;
				return current;
			}
			
			//If we have a non-null value, transform from the value to the next Ref
			current = (Ref<?>)iterator.next().transform(currentValue);

			//This is the next element
			return current;
		}
		
		//Again relying on correct construction
		@SuppressWarnings("unchecked")
		@Override
		public Ref<D> destination() {
			//We can only get the final value by following the final transformer from
			//the last value reached by iterator
			if (hasNext()) {
				throw new NoSuchElementException("Cannot get destination() value until the iterator has provided all values");
			}
			
			//If the current Ref is null, return null
			if (current == null) {
				return null;
			}
			
			//If we don't know the final ref yet, find it by following last transform
			if (finalProp == null) {
				Object currentValue = current.get();

				//If we have a null value in a Ref, then we need to give up - just set
				//finalProp and current to null and return
				if (currentValue == null) {
					current = null;
					finalProp = null;
				} else {
					//If we have a non-null value, transform from the value to the next Ref
					finalProp = (Ref<D>)lastTransform.transform(currentValue);
				}
			}
			
			return finalProp;
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException("Cannot remove() from PathIterator");
		}
		
	}
}
